/*  This file is part of "reprepro"
 *  Copyright (C) 2007 Bernhard R. Link
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02111-1301  USA
 */
#include <config.h>

#include <errno.h>
#include <assert.h>
#include <limits.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>

#include "error.h"
#include "names.h"
#include "configparser.h"

extern int verbose;

struct configiterator {
	FILE *f;
	unsigned int startline, line, column, markerline, markercolumn;
	char *filename;
	bool eol;
};

const char *config_filename(const struct configiterator *iter) {
	return iter->filename;
}

unsigned int config_line(const struct configiterator *iter) {
	return iter->line;
}

unsigned int config_column(const struct configiterator *iter) {
	return iter->column;
}

unsigned int config_firstline(const struct configiterator *iter) {
	return iter->startline;
}

unsigned int config_markerline(const struct configiterator *iter) {
	return iter->markerline;
}

unsigned int config_markercolumn(const struct configiterator *iter) {
	return iter->markercolumn;
}

void config_overline(struct configiterator *iter) {
	int c;

	while( !iter->eol ) {
		c = fgetc(iter->f);
		if( c == '#' ) {
			do {
				c = fgetc(iter->f);
			} while( c != EOF && c != '\n' );
		}
		if( c == EOF || c == '\n' )
			iter->eol = true;
		else
			iter->column++;
	}
}

bool config_nextline(struct configiterator *iter) {
	int c;

	assert( iter->eol );
	c = fgetc(iter->f);
	while( c == '#' ) {
		do {
			c = fgetc(iter->f);
		} while( c != EOF && c != '\n' );
		iter->line++;
		c = fgetc(iter->f);
	}
	if( c == EOF )
		return false;
	if( c == ' ' || c == '\t' ) {
		iter->line++;
		iter->column = 1;
		iter->eol = false;
		return true;
	}
	(void)ungetc(c, iter->f);
	return false;
}

retvalue linkedlistfinish(UNUSED(void *privdata), void *this, void **last, UNUSED(bool complete), UNUSED(struct configiterator *dummy3)) {
	*last = this;
	return RET_NOTHING;
}

static inline retvalue finishchunk(configfinishfunction finishfunc, void *privdata, struct configiterator *iter, const struct configfield *fields, size_t fieldcount, bool *found, void **this, void **last, bool complete) {
	size_t i;
	retvalue r;

	if( complete )
		for( i = 0 ; i < fieldcount ; i++ ) {
			if( !fields[i].required )
				continue;
			if( found[i] )
				continue;
			fprintf(stderr,
"Error parsing config file %s, line %u:\n"
"Required field '%s' expected (since line %u).\n",
					iter->filename, iter->line,
					fields[i].name, iter->startline);
			(void)finishfunc(privdata, *this, last, false, iter);
			*this = NULL;
			return RET_ERROR_MISSING;
		}
	r = finishfunc(privdata, *this, last, complete, iter);
	*this = NULL;
	return r;
}

retvalue configfile_parse(const char *confdir, const char *filename, bool ignoreunknown, configinitfunction initfunc, configfinishfunction finishfunc, const struct configfield *fields, size_t fieldcount, void *privdata) {
	bool found[fieldcount];
	void *last = NULL, *this = NULL;
	char key[100];
	size_t keylen;
	int c, ret;
	size_t i;
	struct configiterator iter;
	retvalue result, r;

	iter.filename = calc_dirconcat(confdir, filename);
	if( iter.filename == NULL )
		return RET_ERROR_OOM;
	iter.line = 0;
	iter.column = 0;

	iter.f = fopen(iter.filename, "r");
	if( iter.f == NULL ) {
		int e = errno;
		fprintf(stderr, "Error opening config file '%s': %s(%d)\n",
				iter.filename, strerror(e), e);
		free(iter.filename);
		return RET_ERRNO(e);
	}
	result = RET_NOTHING;
	do {
		iter.line++;
		iter.column = 1;

		c = fgetc(iter.f);
		while( c == '#' ) {
			do {
				c = fgetc(iter.f);
			} while( c != EOF && c != '\n' );
			iter.line++;
			c = fgetc(iter.f);
		}
		if( c == '\r' )  {
			do {
				c = fgetc(iter.f);
			} while( c == '\r');
			if( c != EOF && c != '\n' ) {
				fprintf(stderr, "%s:%u: error parsing configuration file: CR without following LF!\n",
						iter.filename, iter.line);
				result = RET_ERROR;
				break;
			}
		}
		if( c == EOF )
			break;
		if( c == '\n' ) {
			/* Ignore multiple emptye lines */
			if( this == NULL )
				continue;
			/* finish this chunk, to get ready for the next: */
			r = finishchunk(finishfunc, privdata, &iter,
					fields, fieldcount, found,
					&this, &last, true);
			if( RET_WAS_ERROR(r) ) {
				result = r;
				break;
			}
			continue;
		}
		if( c == '\0' ) {
			fprintf(stderr, "Error parsing %s, line %u: \\000 character not allowed in config files!\n",
					iter.filename, iter.line);
			result = RET_ERROR;
			break;
		}
		if( c == '\0' ) {
			fprintf(stderr, "Error parsing %s, line %u: unexpected white space before keyword!\n",
					iter.filename, iter.line);
			result = RET_ERROR;
			break;
		}
		key[0] = c;
		keylen = 1;

		while( (c = fgetc(iter.f)) != EOF && c != ':' && c != '\n' && c != '#' && c != '\0') {
			iter.column++;
			if( c == ' ' ) {
				fprintf(stderr, "Error parsing %s, line %u: Unexpected space in header name!\n",
						iter.filename, iter.line);
				result = RET_ERROR;
				break;
			}
			if( c == '\t' ) {
				fprintf(stderr, "Error parsing %s, line %u: Unexpected tabulator character in header name!\n",
						iter.filename, iter.line);
				result = RET_ERROR;
				break;
			}
			key[keylen++] = c;
			if( keylen >= 100 )
				break;
		}
		if( c != ':' ) {
			if( c != ' ' && c != '\t' )
				/* newline or end-of-file */
				fprintf(stderr, "Error parsing %s, line %u, column %u: Colon expected!\n",
					iter.filename, iter.line, iter.column);
			result = RET_ERROR;
			break;
		}
		if( this == NULL ) {
			/* new chunk, initialize everything */
			r = initfunc(privdata, last, &this);
			assert( r != RET_NOTHING );
			if( RET_WAS_ERROR(r) ) {
				result = r;
				break;
			}
			assert( this != NULL );
			iter.startline = iter.line;
			memset(found, 0, sizeof(found));
		}
		for( i = 0 ; i < fieldcount ; i++ ) {
			if( keylen != fields[i].namelen )
				continue;
			if( strncasecmp(key, fields[i].name, keylen) != 0 )
				continue;
			break;
		}
		if( i >= fieldcount ) {
			key[keylen] = '\0';
			if( !ignoreunknown ) {
				fprintf(stderr, "Error parsing %s, line %u: Unknown header '%s'!\n",
						iter.filename, iter.line, key);
				result = RET_ERROR_UNKNOWNFIELD;
				break;
			}
			if( verbose >= 0 )
				fprintf(stderr, "Warning parsing %s, line %u: Unknown header '%s'!\n",
						iter.filename, iter.line, key);
		} else if( found[i] ) {
			fprintf(stderr, "Error parsing %s, line %u: Second appearance of '%s' in the same chunk!\n",
					iter.filename, iter.line, fields[i].name);
			result = RET_ERROR;
			break;
		} else
			found[i] = true;
		do {
			c = fgetc(iter.f);
			iter.column++;
		} while( c == ' ' || c == '\t' );
		(void)ungetc(c, iter.f);

		iter.eol = false;
		if( i < fieldcount ) {
			r = fields[i].setfunc(privdata, confdir, fields[i].name, this, &iter);
			RET_UPDATE(result, r);
			if( RET_WAS_ERROR(r) )
				break;
		}
		/* ignore all data left of this field */
		do {
			config_overline(&iter);
		} while( config_nextline(&iter) );
	} while( true );
	if( this != NULL ) {
		r = finishchunk(finishfunc, privdata, &iter,
				fields, fieldcount, found,
				&this, &last,
				!RET_WAS_ERROR(result));
		RET_UPDATE(result, r);
	}
	if( ferror(iter.f) != 0) {
		int e = errno;
		fprintf(stderr, "Error reading config file '%s': %s(%d)\n",
				iter.filename, strerror(e), e);
		r = RET_ERRNO(e);
		RET_UPDATE(result, r);
	}
	ret = fclose(iter.f);
	if( ret != 0 ) {
		int e = errno;
		fprintf(stderr, "Error closing config file '%s': %s(%d)\n",
				iter.filename, strerror(e), e);
		r = RET_ERRNO(e);
		RET_UPDATE(result, r);
	}
	free(iter.filename);
	return result;
}

static inline int config_nextchar(struct configiterator *iter) {
	int c;
	unsigned int realcolumn;

	c = fgetc(iter->f);
	realcolumn = iter->column + 1;
	if( c == '#' ) {
		do {
			c = fgetc(iter->f);
			realcolumn++;
		} while( c != '\n' && c != EOF && c != '\r' );
	}
	if( c == '\r' ) {
		while( c == '\r' ) {
			realcolumn++;
			c = fgetc(iter->f);
		}
		if( c != '\n' && c != EOF ) {
			fprintf(stderr, "Warning parsing config file '%s', line '%u', column %u: CR not followed by LF!\n",
					config_filename(iter),
					config_line(iter),
					realcolumn);

		}
	}
	if( c == EOF ) {
		fprintf(stderr, "Warning parsing config file '%s', line '%u': File ending without final LF!\n",
				config_filename(iter),
				config_line(iter));
		/* fake a proper text file: */
		c = '\n';
	}
	iter->column++;
	if( c == '\n' )
		iter->eol = true;
	return c;
}

static inline int config_nextnonspace(struct configiterator *iter) {
	int c;

	do {
		iter->markerline = iter->line;
		iter->markercolumn = iter->column;
		if( iter->eol ) {
			if( !config_nextline(iter) )
				return EOF;
		}
		c = config_nextchar(iter);
	} while( c == '\n' || c == ' ' || c == '\t');
	return c;
}

int config_nextnonspaceinline(struct configiterator *iter) {
	int c;

	do {
		iter->markerline = iter->line;
		iter->markercolumn = iter->column;
		if( iter->eol )
			return EOF;
		c = config_nextchar(iter);
		if( c == '\n' )
			return EOF;
	} while( c == '\r' || c == ' ' || c == '\t');
	return c;
}

#define configparser_errorlast(iter,message,...) \
	fprintf(stderr, "Error parsing %s, line %u, column %u: " message "\n", \
			iter->filename, iter->markerline, \
			iter->markercolumn, ##  __VA_ARGS__);
#define configparser_error(iter,message,...) \
	fprintf(stderr, "Error parsing %s, line %u, column %u: " message "\n", \
			iter->filename, iter->line, \
			iter->column, ##  __VA_ARGS__);

retvalue config_completeword(struct configiterator *iter, char firstc, char **result_p) {
	size_t size = 0, len = 0;
	char *value = NULL, *nv;
	int c = firstc;

	iter->markerline = iter->line;
	iter->markercolumn = iter->column;
	do {
		if( len + 2 >= size ) {
			nv = realloc(value, size+128);
			if( nv == NULL ) {
				free(value);
				return RET_ERROR_OOM;
			}
			size += 128;
			value = nv;
		}
		value[len] = c;
		len++;
		c = config_nextchar(iter);
		if( c == '\n' )
			break;
	} while( c != ' ' && c != '\t' );
	assert( len > 0 );
	assert( len < size );
	value[len] = '\0';
	nv = realloc(value, len+1);
	if( nv == NULL )
		*result_p = value;
	else
		*result_p = nv;
	return RET_OK;
}

retvalue config_getwordinline(struct configiterator *iter, char **result_p) {
	int c;

	c = config_nextnonspaceinline(iter);
	if( c == EOF )
		return RET_NOTHING;
	return config_completeword(iter, c, result_p);
}

retvalue config_getword(struct configiterator *iter, char **result_p) {
	int c;

	c = config_nextnonspace(iter);
	if( c == EOF )
		return RET_NOTHING;
	return config_completeword(iter, c, result_p);
}

retvalue config_getonlyword(struct configiterator *iter, const char *header, checkfunc check, char **result_p) {
	char *value;
	retvalue r;

	r = config_getword(iter, &value);
	if( r == RET_NOTHING ) {
		configparser_errorlast(iter,
"Unexpected end of %s header (value expected).", header);
		return RET_ERROR;
	}
	if( RET_WAS_ERROR(r) )
		return r;
	if( config_nextnonspace(iter) != EOF ) {
		configparser_error(iter,
"End of %s header expected (but trailing garbage).", header);
		free(value);
		return RET_ERROR;
	}
	if( check != NULL ) {
		const char *errormessage = check(value);
		if( errormessage != NULL ) {
			configparser_errorlast(iter,
"Malformed %s header content '%s': %s\n", header, value, errormessage);
			free(value);
			checkerror_free(errormessage);
			return RET_ERROR;
		}
	}
	*result_p = value;
	return RET_OK;
}

retvalue config_getuniqwords(struct configiterator *iter, const char *header, checkfunc check, struct strlist *result_p) {
	char *value;
	retvalue r;
	struct strlist data;
	const char *errormessage;

	strlist_init(&data);
	while( (r = config_getword(iter, &value)) != RET_NOTHING ) {
		if( RET_WAS_ERROR(r) ) {
			strlist_done(&data);
			return r;
		}
		if( strlist_in(&data, value) ) {
			configparser_errorlast(iter,
"Unexpected duplicate '%s' within %s header.", value, header);
			free(value);
			strlist_done(&data);
			return RET_ERROR;
		} else if( check != NULL && (errormessage = check(value)) != NULL ) {
			configparser_errorlast(iter,
"Malformed %s header element '%s': %s\n", header, value, errormessage);
			checkerror_free(errormessage);
			free(value);
			strlist_done(&data);
			return RET_ERROR;
		} else {
			r = strlist_add(&data, value);
			if( RET_WAS_ERROR(r) ) {
				strlist_done(&data);
				return r;
			}
		}
	}
	strlist_move(result_p, &data);
	return RET_OK;
}

retvalue config_getwords(struct configiterator *iter, struct strlist *result_p) {
	char *value;
	retvalue r;
	struct strlist data;

	strlist_init(&data);
	while( (r = config_getword(iter, &value)) != RET_NOTHING ) {
		if( RET_WAS_ERROR(r) ) {
			strlist_done(&data);
			return r;
		}
		r = strlist_add(&data, value);
		if( RET_WAS_ERROR(r) ) {
			strlist_done(&data);
			return r;
		}
	}
	strlist_move(result_p, &data);
	return RET_OK;
}

retvalue config_getsplitwords(struct configiterator *iter, UNUSED(const char *header), struct strlist *from_p, struct strlist *into_p) {
	char *value, *origin, *destination, *separator;
	retvalue r;
	struct strlist data_from, data_into;

	strlist_init(&data_from);
	strlist_init(&data_into);
	while( (r = config_getword(iter, &value)) != RET_NOTHING ) {
		if( RET_WAS_ERROR(r) ) {
			strlist_done(&data_from);
			strlist_done(&data_into);
			return r;
		}
		separator = strchr(value, '>');
		if( separator == NULL ) {
			destination = strdup(value);
			origin = value;
		} else if( separator == value ) {
			destination = strdup(separator+1);
			origin = strdup(separator+1);
			free(value);
		} else if( separator[1] == '\0' ) {
			*separator = '\0';
			destination = strdup(value);
			origin = value;
		} else {
			origin = strndup(value, separator-value);
			destination = strdup(separator+1);
			free(value);
		}
		if( origin == NULL || destination == NULL ) {
			free(origin);free(destination);
			strlist_done(&data_from);
			strlist_done(&data_into);
			return RET_ERROR_OOM;
		}
		r = strlist_add(&data_from, origin);
		if( RET_WAS_ERROR(r) ) {
			free(destination);
			strlist_done(&data_from);
			strlist_done(&data_into);
			return r;
		}
		r = strlist_add(&data_into, destination);
		if( RET_WAS_ERROR(r) ) {
			strlist_done(&data_from);
			strlist_done(&data_into);
			return r;
		}
	}
	strlist_move(from_p, &data_from);
	strlist_move(into_p, &data_into);
	return RET_OK;
}

retvalue config_getconstant(struct configiterator *iter, const struct constant *constants, int *result_p) {
	retvalue r;
	char *value;
	const struct constant *c;

	/* that could be done more in-situ, but is not runtime-critical at all */

	r = config_getword(iter, &value);
	if( r == RET_NOTHING )
		return r;
	if( RET_WAS_ERROR(r) )
		return r;
	for( c = constants ; c->name != NULL ; c++ ) {
		if( strcmp(c->name, value) == 0 ) {
			free(value);
			*result_p = c->value;
			return RET_OK;
		}
	}
	free(value);
	return RET_ERROR_UNKNOWNFIELD;
}

retvalue config_getflags(struct configiterator *iter, const char *header, const struct constant *constants, bool *flags, bool ignoreunknown, const char *msg) {
	retvalue r, result = RET_NOTHING;
	int option = -1;

	while( (r = config_getconstant(iter, constants, &option)) != RET_NOTHING ) {
		if( r == RET_ERROR_UNKNOWNFIELD ) {
// TODO: would be nice to have the wrong flag here to put it in the error message:
			if( ignoreunknown ) {
				fprintf(stderr,
"Warning: ignored error parsing config file %s, line %u, column %u:\n"
"Unknown flag in %s header.%s\n",
					config_filename(iter),
					config_markerline(iter),
					config_markercolumn(iter),
					header, msg);
				continue;
			}
			fprintf(stderr,
"Error parsing config file %s, line %u, column %u:\n"
"Unknown flag in %s header.%s\n",
					config_filename(iter),
					config_markerline(iter),
					config_markercolumn(iter),
					header, msg);
		}
		if( RET_WAS_ERROR(r) )
			return r;
		assert( option >= 0 );
		flags[option] = true;
		result = RET_OK;
		option = -1;
	}
	return result;
}

retvalue config_getall(struct configiterator *iter, char **result_p) {
	size_t size = 0, len = 0;
	char *value = NULL, *nv;
	int c;

	c = config_nextnonspace(iter);
	if( c == EOF )
		return RET_NOTHING;
	iter->markerline = iter->line;
	iter->markercolumn = iter->column;
	do {
		if( len + 2 >= size ) {
			nv = realloc(value, size+128);
			if( nv == NULL ) {
				free(value);
				return RET_ERROR_OOM;
			}
			size += 128;
			value = nv;
		}
		value[len] = c;
		len++;
		if( iter->eol ) {
			if( !config_nextline(iter) )
				break;
		}
		c = config_nextchar(iter);
	} while( true );
	assert( len > 0 );
	assert( len < size );
	while( len > 0 && ( value[len-1] == ' ' || value[len-1] == '\t' ||
			    value[len-1] == '\n' || value[len-1] == '\r' ) )
		len--;
	value[len] = '\0';
	nv = realloc(value, len+1);
	if( nv == NULL )
		*result_p = value;
	else
		*result_p = nv;
	return RET_OK;
}

retvalue config_gettruth(struct configiterator *iter, const char *header, bool *result_p) {
	char *value = NULL;
	retvalue r;

	/* wastefull, but does not happen that often */

	r = config_getword(iter, &value);
	if( r == RET_NOTHING ) {
		configparser_errorlast(iter,
"Unexpected empty boolean %s header (something like Yes or No expected).", header);
		return RET_ERROR;
	}
	if( RET_WAS_ERROR(r) )
		return r;
	// TODO: check against trailing garbage
	if( strcasecmp(value, "Yes") == 0 ) {
		*result_p = true;
		free(value);
		return RET_OK;
	}
	if( strcasecmp(value, "No") == 0 ) {
		*result_p = false;
		free(value);
		return RET_OK;
	}
	if( strcmp(value, "1") == 0 ) {
		*result_p = true;
		free(value);
		return RET_OK;
	}
	if( strcmp(value, "0") == 0 ) {
		*result_p = false;
		free(value);
		return RET_OK;
	}
	configparser_errorlast(iter,
"Unexpected value in boolean %s header (something like Yes or No expected).", header);
	free(value);
	return RET_ERROR;
}

retvalue config_getnumber(struct configiterator *iter, const char *name, long long *result_p, long long minval, long long maxval) {
	char *word = NULL;
	retvalue r;
	long long value;
	char *e;

	r = config_getword(iter, &word);
	if( r == RET_NOTHING ) {
		configparser_errorlast(iter,
"Unexpected end of line (%s number expected).", name);
		return RET_ERROR;
	}
	if( RET_WAS_ERROR(r) )
		return r;

	value = strtoll(word, &e, 10);
	if( e == word ) {
		fprintf(stderr,
"Error parsing config file %s, line %u, column %u:\n"
"Expected %s number but got '%s'\n",
			config_filename(iter), config_markerline(iter),
			config_markercolumn(iter), name, word);
		free(word);
		return RET_ERROR;
	}
	if( e != NULL && *e != '\0' ) {
		unsigned char digit1, digit2, digit3;
		digit1 = ((unsigned char)(*e))&0x7;
		digit2 = (((unsigned char)(*e)) >> 3)&0x7;
		digit3 = (((unsigned char)(*e)) >> 6)&0x7;
		fprintf(stderr,
"Error parsing config file %s, line %u, column %u:\n"
"Unexpected character \\%01hhu%01hhu%01hhu in %s number '%s'\n",
			config_filename(iter), config_markerline(iter),
			config_markercolumn(iter) + (int)(e-word),
			digit3, digit2, digit1,
			name, word);
		free(word);
		return RET_ERROR;
	}
	if( value == LLONG_MAX || value > maxval) {
		fprintf(stderr,
"Error parsing config file %s, line %u, column %u:\n"
"Too large %s number '%s'\n",
			config_filename(iter), config_markerline(iter),
			config_markercolumn(iter), name, word);
		free(word);
		return RET_ERROR;
	}
	if( value == LLONG_MIN || value < minval ) {
		fprintf(stderr,
"Error parsing config file %s, line %u, column %u:\n"
"Too small %s number '%s'\n",
			config_filename(iter), config_markerline(iter),
			config_markercolumn(iter), name, word);
		free(word);
		return RET_ERROR;
	}
	free(word);
	*result_p = value;
	return RET_OK;
}

static retvalue config_getline(struct configiterator *iter, /*@out@*/char **result_p) {
	size_t size = 0, len = 0;
	char *value = NULL, *nv;
	int c;

	c = config_nextnonspace(iter);
	if( c == EOF )
		return RET_NOTHING;
	iter->markerline = iter->line;
	iter->markercolumn = iter->column;
	do {
		if( len + 2 >= size ) {
			nv = realloc(value, size+128);
			if( nv == NULL ) {
				free(value);
				return RET_ERROR_OOM;
			}
			size += 128;
			value = nv;
		}
		value[len] = c;
		len++;
		c = config_nextchar(iter);
	} while( c != '\n' );
	assert( len > 0 );
	assert( len < size );
	while( len > 0 &&
			(value[len-1] == ' ' || value[len-1] == '\t'
			 || value[len-1] == '\r' ) )
		len--;
	assert( len > 0 );
	value[len] = '\0';
	nv = realloc(value, len+1);
	if( nv == NULL )
		*result_p = value;
	else
		*result_p = nv;
	return RET_OK;
}

retvalue config_getlines(struct configiterator *iter, struct strlist *result) {
	char *line;
	struct strlist list;
	retvalue r;

	strlist_init(&list);
	do {
		r = config_getline(iter, &line);
		if( RET_WAS_ERROR(r) ) {
			strlist_done(&list);
			return r;
		}
		if( r == RET_NOTHING )
			r = strlist_add_dup(&list, "");
		else
			r = strlist_add(&list, line);
		if( RET_WAS_ERROR(r) ) {
			strlist_done(&list);
			return r;
		}
	} while( config_nextline(iter) );
	strlist_move(result, &list);
	return RET_OK;
}
